package com.dana1.nos.old.impl;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.TreeMap;

import org.apache.commons.lang.StringUtils;

import com.dana1.nos.old.ObjectKey;
import com.dana1.nos.old.ObjectService;
import com.dana1.nos.old.ServiceObject;
import com.dana1.nos.old.meta.GeneratedBy;
import com.dana1.nos.old.meta.InverseOf;
import com.dana1.nos.old.meta.Serialized;
import com.dana1.nos.old.util.HardMemoizer;
import com.dana1.nos.old.util.Producer;
import com.dana1.nos.old.util.PropertyUtils;

public class ServiceObjectInvocationHandler<T extends ServiceObject> implements InvocationHandler
{
	private ObjectService objectService;
	private TreeMap<String,Object> propertyValues = new TreeMap<String,Object>();
	private Class<T> proxyClass;
	private boolean connected = false;
	private boolean autoInsertServiceObjectProperties = true;
	
	public ServiceObjectInvocationHandler(ObjectService objectService,Class<T> proxyClass)
	{
		this.objectService = objectService;
		this.proxyClass = proxyClass;
	}
	public boolean isConnected()
	{
		return connected;
	}
	public void setConnected(boolean connected)
	{
		this.connected = connected;
	}

	public boolean isAutoInsertServiceObjectProperties()
	{
		return autoInsertServiceObjectProperties;
	}
	public void setAutoInsertServiceObjectProperties(boolean autoInsertServiceObjectProperties)
	{
		this.autoInsertServiceObjectProperties = autoInsertServiceObjectProperties;
	}
	
	public TreeMap<String, Object> getPropertyValues()
	{
		return propertyValues;
	}
	public void setGeneratedFields(Object proxy) 
	{
		try
		{
			for (PropertyDescriptor propDesc: generatedFields())
				if (!propertyValues.containsKey(propDesc.getName()))
					propertyValues.put(propDesc.getName(), getGettorAnnotation(propDesc, GeneratedBy.class).value().newInstance().nextValue(proxyClass));
		} 
		catch (Exception e)
		{
			throw new RuntimeException(e.getMessage(),e);
		}
	}
	
	public ObjectService getObjectService()
	{
		return objectService;
	}
	@Override
	public synchronized Object invoke(Object proxy, Method method, Object [] args) throws Throwable
	{
		if (method.getName().equals("hashCode"))
			return hashCode(proxy,args);
		if (method.getName().equals("equals"))
			return equals(proxy,args);
		if (method.getName().equals("compareTo"))
			return compareTo(proxy,args);
		if (method.getName().equals("toString"))
			return toString(proxy,args);
		if (method.getDeclaringClass().equals(Object.class))
			return getClass().getMethod(method.getName()).invoke(this,args);
		if (method.getDeclaringClass().equals(Comparable.class))
			return getClass().getMethod(method.getName()).invoke(this,args);
		if (method.getDeclaringClass().equals(ServiceObject.class))
			return getClass().getMethod(method.getName()).invoke(this,args);
		
		return invokeUsingIntrospection(proxy, method, args);
	}

	
	public Class<T> getProxyClass()
	{
		return proxyClass;
	}
	private PropertyDescriptor [] generatedFields()
	{
		return HardMemoizer.memoize(new Producer<PropertyDescriptor []>() {
			@Override
			public PropertyDescriptor[] produce()
			{
				ArrayList<PropertyDescriptor> result = new ArrayList<PropertyDescriptor>();
				@SuppressWarnings("unused")
				GeneratedBy genBy;
				for (PropertyDescriptor propDesc: PropertyUtils.getPropertyDescriptors(proxyClass, Object.class))
					if (null!=(genBy=getGettorAnnotation(propDesc, GeneratedBy.class)))
						result.add(propDesc);
				return result.toArray(new PropertyDescriptor [0]);
			}
		}, ServiceObjectInvocationHandler.class,proxyClass,"generatedFields");
	}
	private Object invokeUsingIntrospection(Object proxy, Method method, Object[] args) throws IntrospectionException
	{
		PropertyDescriptor propDesc = PropertyUtils.getPropertyDescriptor(proxyClass, Object.class, propertyNameFromMethod(method));
		if (matchesReadMethod(method, propDesc))
			return getProperty(proxy,propDesc);
		else if (matchesWriteMethod(method, propDesc))
			return setProperty(proxy,propDesc,args[0]);

		throw new UnsupportedOperationException();
	}
	private boolean matchesWriteMethod(Method method, PropertyDescriptor propDesc)
	{
		return (null!=propDesc.getWriteMethod())&&method.getName().equals(propDesc.getWriteMethod().getName());
	}
	private boolean matchesReadMethod(Method method, PropertyDescriptor propDesc)
	{
		return (null!=propDesc.getReadMethod())&&method.getName().equals(propDesc.getReadMethod().getName());
	}
	private String propertyNameFromMethod(Method method)
	{
		return StringUtils.uncapitalize(method.getName().replaceFirst("^(?:(?:get)|(?:set)|(?:is))", ""));
	}
	private Object getProperty(Object proxy, PropertyDescriptor propDesc)
	{
		Class<?> propertyType = propDesc.getPropertyType();

		if (ServiceObject.class.isAssignableFrom(propertyType))
			return getServiceObjectProperty(proxy,propDesc);
		if (Collection.class.isAssignableFrom(propertyType))
			return getCollectionProperty(proxy,propDesc);

		return propertyValues.get(propDesc.getName());
	}
	
	private Object setProperty(Object proxy, PropertyDescriptor propDesc, Object arg)
	{
		Class<?> propertyType = propDesc.getPropertyType();
	
		if (ServiceObject.class.isAssignableFrom(propertyType))
			setServiceObjectProperty(proxy,propDesc,arg);
		else if (Collection.class.isAssignableFrom(propertyType))
			setCollectionProperty(proxy,propDesc,arg);
		else
			propertyValues.put(propDesc.getName(), arg);
		
		objectService.update((ServiceObject)proxy);
		
		return null;
	}

	private Object getServiceObjectProperty(Object proxy, PropertyDescriptor propDesc)
	{
		return objectService.retrieve((ObjectKey<?>)propertyValues.get(propDesc.getName()));
	}
	
	private void setServiceObjectProperty(Object proxy, PropertyDescriptor propDesc, Object arg)
	{
		ObjectKey<?> key = maybeInsertReferencedObject(arg);

		InverseOf io = getGettorAnnotation(propDesc, InverseOf.class);
		if (null==io)
			propertyValues.put(propDesc.getName(), key);
		else if (Collection.class.isAssignableFrom(getInversePropertyDescriptor(propDesc,io).getPropertyType()))
			objectService.add1M(key, propDesc.getName(), objectService.objectKey((ServiceObject)proxy));
		else
			objectService.set11(objectService.objectKey((ServiceObject)proxy), propDesc.getName(), key);
	}

	private ObjectKey<?> maybeInsertReferencedObject(Object arg)
	{
		ObjectKey<?> key = null;
		if (arg!=null)
		{
			if (!((ServiceObjectInvocationHandler<?>)Proxy.getInvocationHandler(arg)).isConnected())
				if (isAutoInsertServiceObjectProperties())
					key=objectService.insert((ServiceObject)arg);
				else
					throw new UnsupportedOperationException("Cannot set an indirect object property for an object that has not been inserted");
			key = objectService.objectKey((ServiceObject)arg);
		}
		return key;
	}
	
	private Class<?> getInverseType(Class<?> proxyClass, PropertyDescriptor propDesc) 
	{
		try
		{
			Method interfaceMethodForPropDesc = PropertyUtils.getInterfaceMethod(proxyClass,propDesc);
			Class<?> inverseType = (Class<?>)interfaceMethodForPropDesc.getReturnType();
			if (Collection.class.isAssignableFrom(inverseType))
			{
				Type t = interfaceMethodForPropDesc.getGenericReturnType();
				if (t instanceof ParameterizedType)
					inverseType = (Class<?>)((ParameterizedType)t).getActualTypeArguments()[0];
			}
			return inverseType;
		}
		catch (Exception e)
		{
			throw new RuntimeException(e.getMessage(),e);
		}
	}
	
	private PropertyDescriptor getInversePropertyDescriptor(PropertyDescriptor propDesc,InverseOf io)
	{
		return PropertyUtils.getPropertyDescriptor(getInverseType(proxyClass,propDesc), Object.class, io.value());
	}
	
	private void setCollectionProperty(Object proxy, PropertyDescriptor propDesc, Object arg)
	{	
		if (null!=getGettorAnnotation(propDesc, Serialized.class))
		{
			propertyValues.put(propDesc.getName(), arg);
			return;
		}
		throw new UnsupportedOperationException("Collection values cannot be set directly");
	}

	@SuppressWarnings("unchecked")
	private Collection<?> getCollectionProperty(Object proxy, PropertyDescriptor propDesc)
 	{
 		if (null!=getGettorAnnotation(propDesc, Serialized.class))
 			return (Collection<?>)propertyValues.get(propDesc.getName());

 		if (!isConnected())
 			throw new UnsupportedOperationException("Cannot obtain a relational collection until the owning object has been inserted");
 		
		InverseOf io = getGettorAnnotation(propDesc, InverseOf.class);
		if (null==io)
	 		return new ProxyMMCollection(objectService, objectService.objectKey((ServiceObject)proxy), deriveRelationName(proxy,propDesc), autoInsertServiceObjectProperties);

		PropertyDescriptor invDesc = getInversePropertyDescriptor(propDesc, io);
		if (Collection.class.isAssignableFrom(invDesc.getPropertyType()))
	 		return new ProxyMMCollection(objectService, objectService.objectKey((ServiceObject)proxy), deriveRelationName(proxy,propDesc), autoInsertServiceObjectProperties);
		else if (ServiceObject.class.isAssignableFrom(invDesc.getPropertyType()))
			return new Proxy1MCollection(objectService, objectService.objectKey((ServiceObject)proxy), (Class<? extends ServiceObject>)getInverseType(proxyClass,propDesc), invDesc.getName(), autoInsertServiceObjectProperties);
		else
			throw new UnsupportedOperationException("The target of an inverse collection must be either a collection or a service object type.");
		
 	}
	
	private <S extends Annotation> S getGettorAnnotation(PropertyDescriptor propDesc, Class<S> annoClass)
	{
		return  PropertyUtils.getInterfaceMethod(proxyClass,propDesc).getAnnotation(annoClass);
	}

	@SuppressWarnings("unused")
	private <S extends Annotation> S getSettorAnnotation(PropertyDescriptor propDesc, Class<S> annoClass)
	{
		try
		{
			return  proxyClass.getMethod(propDesc.getWriteMethod().getName(),new Class<?>[] { propDesc.getPropertyType() } ).getAnnotation(annoClass);
		}
		catch (Exception e)
		{
			throw new RuntimeException(e.getMessage(),e);
		}
	}
	
	/** if the relation has an inverse, its the alphabetized classnames plus first classes relation name
	 * 
	 * @param proxy
	 * @param propDesc
	 * @return
	 */
	private String deriveRelationName(Object proxy, PropertyDescriptor propDesc)
	{
		InverseOf inverseOf = PropertyUtils.getInterfaceMethod(proxyClass,propDesc).getAnnotation(InverseOf.class);
		if (null==inverseOf)
			return proxyClass.getName()+"::"+propDesc.getName();
		
		String proxyClassName = proxyClass.getName();
		String inverseClassName = getInverseType(proxyClass,propDesc).getName();

		String result;
		if (proxyClassName.compareTo(inverseClassName)<=0)
			result = StringUtils.join(new String [] {proxyClassName,inverseClassName,propDesc.getName()},"::");
		else
			result = StringUtils.join(new String [] {inverseClassName,proxyClassName,StringUtils.uncapitalize(inverseOf.value())},"::");
		return result;
	}

	public int hashCode(Object proxy,Object [] args)	
	{
		return objectService.objectKey((ServiceObject)proxy).hashCode();
	}
	public boolean equals(Object proxy, Object [] args) 
	{
		if (args[0] instanceof ServiceObject)
			return objectService.objectKey((ServiceObject)proxy).equals(objectService.objectKey((ServiceObject)args[0]));
		return false;
	}
	public String toString(Object proxy, Object [] args) 
	{
		return proxyClass.getSimpleName()+"["+propertyValues.toString()+"]";
	}
	public int compareTo(Object proxy, Object [] args) 
	{
		return objectService.objectKey((ServiceObject)proxy).compareTo(objectService.objectKey((ServiceObject)args[1]));
	}

}
